﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;

namespace PrisonStep
{
    public abstract class ParticleSystem : DrawableGameComponent
    {
        #region MemberVariables
        protected string TextureName = null;

        protected int mMaxParticles = 100;

        protected TimeSpan mDuration = TimeSpan.FromSeconds(1);

        protected float mDurationRandomness = 0;

        protected float mEmitterVelocitySensitivity = 1;

        protected float mMinHorizontalVelocity = 0;

        protected float mMaxHorizontalVelocity = 0;

        protected float mMinVerticalVelocity = 0;

        protected float mMaxVerticleVelocity = 0;

        protected Vector3 mGravity = Vector3.Zero;

        protected float mEndVelocity = 1;

        protected Color MinColor = Color.White;

        protected Color MaxColor = Color.White;

        protected float mMinRotateSpeed = 0;

        protected float mMaxRotateSpeed = 0;

        protected float mMinStartSize = 100;

        protected float mMaxStartSize = 100;

        protected float mMinEndSize = 100;

        protected float mMaxEndSize = 100;

        protected BlendState mBlendState;

        private ContentManager mContent;

        private Effect mParticleEffect;

        private EffectParameter mEffectViewParamter;

        private EffectParameter mEffectProjectionParameter;

        private EffectParameter mEffectViewporScaleParamter;

        private EffectParameter mEffectTimeParameter;

        private ParticleVertex[] mParticles;

        private DynamicVertexBuffer mVertexBuffer;

        private IndexBuffer mIndexBuffer;

        private int mFirstActiveParticle;
        private int mFirstNewParticle;
        private int mFirstFreeParticle;
        private int mFirstRetiredParticle;

        private float mCurrentTime;

        private int mDrawCounter;

        static Random mRandom = new Random();

        #endregion

        #region Properties

        #endregion

        protected ParticleSystem(Game aGame, ContentManager aContent)
            : base(aGame)
        {
            mContent = aContent;
        }

        public override void Initialize()
        {
            InitializeSettings();
            mParticles = new ParticleVertex[mMaxParticles * 4];

            for (int i = 0; i < mMaxParticles; i++)
            {
                mParticles[i * 4 + 0].mCorner = new Short2(-1, 1);
                mParticles[i * 4 + 1].mCorner = new Short2(1, -1);
                mParticles[i * 4 + 2].mCorner = new Short2(1, 1);
                mParticles[i * 4 + 3].mCorner = new Short2(-1, 1);
            }
            
            base.Initialize();
        }

        protected abstract void InitializeSettings();

        private void LoadParticleEffect()
        {
            Effect lEffect = mContent.Load<Effect>("ParticleEffect");

            mParticleEffect = lEffect.Clone();

            EffectParameterCollection lParameters = mParticleEffect.Parameters;

            mEffectViewParamter = lParameters["View"];
            mEffectProjectionParameter = lParameters["Projection"];
            mEffectViewporScaleParamter = lParameters["ViewportScale"];
            mEffectTimeParameter = lParameters["CurrentTime"];

            lParameters["Duration"].SetValue((float)mDuration.TotalSeconds);
            lParameters["DurationRandomness"].SetValue(mDurationRandomness);
            lParameters["Gravity"].SetValue(mGravity);
            lParameters["EndVelocity"].SetValue(mEndVelocity);
            lParameters["MinColor"].SetValue(MinColor.ToVector4());
            lParameters["MaxColor"].SetValue(MaxColor.ToVector4());
            lParameters["RotateSpeed"].SetValue(
                    new Vector2(mMinRotateSpeed, mMaxRotateSpeed));
            lParameters["StartSize"].SetValue(
                    new Vector2(mMinStartSize, mMaxStartSize));
            lParameters["EndSize"].SetValue(
                    new Vector2(mMinEndSize, mMaxEndSize));
            Texture2D lTexture = mContent.Load<Texture2D>(TextureName);
            lParameters["Texture"].SetValue(lTexture);
        }

        protected override void LoadContent()
        {
            LoadParticleEffect();

            mVertexBuffer = new DynamicVertexBuffer(GraphicsDevice, ParticleVertex.VertexDeclaration,
                                                    mMaxParticles * 4, BufferUsage.WriteOnly);

            ushort[] lIndices = new ushort[mMaxParticles * 6];

            for (int i = 0; i < mMaxParticles; i++)
            {
                lIndices[i * 6 + 0] = (ushort)(i * 4 + 0);
                lIndices[i * 6 + 1] = (ushort)(i * 4 + 1);
                lIndices[i * 6 + 2] = (ushort)(i * 4 + 2);

                lIndices[i * 6 + 3] = (ushort)(i * 4 + 0);
                lIndices[i * 6 + 4] = (ushort)(i * 4 + 2);
                lIndices[i * 6 + 5] = (ushort)(i * 4 + 3);
            }

            mIndexBuffer = new IndexBuffer(GraphicsDevice, typeof(ushort), lIndices.Length, BufferUsage.WriteOnly);

            mIndexBuffer.SetData(lIndices);
            //base.LoadContent();
        }

        public override void Update(GameTime gameTime)
        {
            mCurrentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

            RetireActiveParticles();
            ClearRetiredParticles();

            if (mFirstActiveParticle == mFirstFreeParticle)
                mCurrentTime = 0;
            if (mFirstRetiredParticle == mFirstActiveParticle)
                mDrawCounter = 0;


            //base.Update(gameTime);
        }

        private void RetireActiveParticles()
        {
            float lParticleLife = (float)mDuration.TotalSeconds;

            while (mFirstActiveParticle != mFirstNewParticle)
            {
                float lParticleAge = mCurrentTime - mParticles[mFirstActiveParticle * 4].mTime;

                if (lParticleAge < lParticleLife)
                    break;
                mParticles[mFirstActiveParticle * 4].mTime = mDrawCounter;

                mFirstActiveParticle++;

                if (mFirstActiveParticle >= mMaxParticles)
                    mFirstActiveParticle = 0;
            }
        }

        private void ClearRetiredParticles()
        {
            while (mFirstRetiredParticle != mFirstActiveParticle)
            {
                int lAge = mDrawCounter - (int)mParticles[mFirstRetiredParticle * 4].mTime;

                if (lAge < 3)
                    break;

                mFirstRetiredParticle++;

                if (mFirstRetiredParticle >= mMaxParticles)
                    mFirstRetiredParticle = 0;
            }
        }

        public override void Draw(GameTime gameTime)
        {
            GraphicsDevice lDevice = GraphicsDevice;

            if (mVertexBuffer.IsContentLost)
                mVertexBuffer.SetData(mParticles);

            if (mFirstNewParticle != mFirstFreeParticle)
                AddNewParticlesToVertexBuffer();

            if (mFirstActiveParticle != mFirstFreeParticle)
            {
                lDevice.BlendState = mBlendState;
                lDevice.DepthStencilState = DepthStencilState.DepthRead;

                mEffectViewporScaleParamter.SetValue(new Vector2(0.5f / lDevice.Viewport.AspectRatio, -0.5f));

                mEffectTimeParameter.SetValue(mCurrentTime);

                lDevice.SetVertexBuffer(mVertexBuffer);
                lDevice.Indices = mIndexBuffer;

                foreach (EffectPass lPass in mParticleEffect.CurrentTechnique.Passes)
                {
                    lPass.Apply();
                    if (mFirstActiveParticle < mFirstFreeParticle)
                    {
                        lDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                      mFirstActiveParticle * 4, (mFirstFreeParticle - mFirstActiveParticle) * 4,
                                                      mFirstActiveParticle * 6, (mFirstFreeParticle - mFirstActiveParticle) * 2);
                    }
                    else
                    {
                        lDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                      mFirstActiveParticle * 4, (mMaxParticles - mFirstActiveParticle) * 4,
                                                      mFirstActiveParticle * 6, (mMaxParticles - mFirstActiveParticle) * 2);
                        if (mFirstFreeParticle > 0)
                        {
                            lDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                          0, mFirstFreeParticle * 4,
                                                          0, mFirstFreeParticle * 2);
                        }
                    }
                }

                lDevice.DepthStencilState = DepthStencilState.Default;
            }
            mDrawCounter++;
            //base.Draw(gameTime);
        }

        void AddNewParticlesToVertexBuffer()
        {
            int stride = ParticleVertex.mByteSize;

            if (mFirstNewParticle < mFirstFreeParticle)
            {
                // If the new particles are all in one consecutive range,
                // we can upload them all in a single call.
                mVertexBuffer.SetData(mFirstNewParticle * stride * 4, mParticles,

                                     mFirstNewParticle * 4,
                                     (mFirstFreeParticle - mFirstNewParticle) * 4,
                                     stride, SetDataOptions.NoOverwrite);
            }
            else
            {
                // If the new particle range wraps past the end of the queue
                // back to the start, we must split them over two upload calls.
                mVertexBuffer.SetData(mFirstNewParticle * stride * 4, mParticles,

                                     mFirstNewParticle * 4,
                                     (mMaxParticles - mFirstNewParticle) * 4,
                                     stride, SetDataOptions.NoOverwrite);

                if (mFirstFreeParticle > 0)
                {
                    mVertexBuffer.SetData(0, mParticles,
                                         0, mFirstFreeParticle * 4,
                                         stride, SetDataOptions.NoOverwrite);
                }
            }

            // Move the particles we just uploaded from the new to the active queue.
            mFirstNewParticle = mFirstFreeParticle;
        }

        public void SetCamera(Matrix aView, Matrix aProjection)
        {
            mEffectViewParamter.SetValue(aView);
            mEffectProjectionParameter.SetValue(aProjection);
        }

        public void AddParticle(Vector3 aPosition, Vector3 aVelocity)
        {
            int lNextFreeParticle = mFirstFreeParticle + 1;

            if (lNextFreeParticle >= mMaxParticles)
                lNextFreeParticle = 0;
            if (lNextFreeParticle == mFirstRetiredParticle)
                return;
            aVelocity *= mEmitterVelocitySensitivity;

            float lHorizontalVelocity = MathHelper.Lerp(mMinHorizontalVelocity,
                                                        mMaxHorizontalVelocity,
                                                        (float)mRandom.NextDouble());

            double lHorizontalAngle = mRandom.NextDouble() * MathHelper.TwoPi;

            aVelocity.X += lHorizontalVelocity * (float)Math.Cos(lHorizontalAngle);
            aVelocity.Z += lHorizontalVelocity * (float)Math.Sin(lHorizontalAngle);


            aVelocity.Y += MathHelper.Lerp(mMinVerticalVelocity,
                                           mMaxVerticleVelocity,
                                           (float)mRandom.NextDouble());
            Color lRandomValues = new Color((byte)mRandom.Next(255),
                                            (byte)mRandom.Next(255),
                                            (byte)mRandom.Next(255),
                                            (byte)mRandom.Next(255));

            for (int i = 0; i < 4; i++)
            {
                mParticles[mFirstFreeParticle * 4 + i].mPosition = aPosition;
                mParticles[mFirstFreeParticle * 4 + i].mVelocity = aVelocity;
                mParticles[mFirstFreeParticle * 4 + i].mRandom = lRandomValues;
                mParticles[mFirstFreeParticle * 4 + i].mTime = mCurrentTime;
            }

            mFirstFreeParticle = lNextFreeParticle;
        }
    }
}
